package server;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.BindException;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Scanner;

import javax.swing.JEditorPane;

public class S_BackEnd {

	/* Variables */
	//users
	//  - list of users with their hostnames and status
	//connection stuff
	
	// GOAL all friends known upon login, because of the size of company
	// GOAL admin user for Server to add new users, new GUI stuff.
	// GOAL code blocks in messages, formatted blocks of code which detect language?
	// GOAL encryption of files
	
	public ArrayList<String[]> users; // TODO write new users to users.txt file for later use when starting up server.
	public Hashtable<String, ArrayList<String[]>> userFriends; // TODO write new friends to friends.txt file for later use when starting up server.
	private String[] statusArray;
	private ServerSocket ss;
	private Socket s;
	private JEditorPane serverLog;
	
	// A mapping from sockets to DataOutputStreams. This will
	// help us avoid having to create a DataOutputStream each time
	// we want to write to a stream.
	private Hashtable<Socket, DataOutputStream> outputStreams;
	private Hashtable<String, DataOutputStream> outputStreamsToUsers;

	//username, hostname, email
	
	/* Constructor */
	
	protected S_BackEnd(JEditorPane eP) {
		setServerLog(eP);
		
		outputStreams = new Hashtable<Socket, DataOutputStream>();
		outputStreamsToUsers = new Hashtable<String, DataOutputStream>();
		
		serverLog.setText(serverLog.getText()+"S_BackEnd -> Intialising server variables...\n");
		users = new ArrayList<String[]>(); //read in users
		
		statusArray = new String[4];
		statusArray[0] = "Online";
		statusArray[1] = "Unavailable";
		statusArray[2] = "Busy";
		statusArray[3] = "Offline";
		
		serverLog.setText(serverLog.getText()+"Attempting to read in user data...\n");
		FileInputStream fUsers = null;
		try {
			fUsers = new FileInputStream("users.txt");
			serverLog.setText(serverLog.getText()+"S_BackEnd -> SUCCESS >>> user data successfully read in.\n");
		} 
		catch (FileNotFoundException e) {
			serverLog.setText(serverLog.getText()+"S_BackEnd -> ERROR >>> unable to locate users.txt.\n");
			e.printStackTrace();
		}
		Scanner userScanner = new Scanner(fUsers);

		serverLog.setText(serverLog.getText()+"S_BackEnd -> Attempting to parse user data to internal variable...\n");
		while (userScanner.hasNext()) {
			String path = userScanner.next();

			Scanner lineScanner = new Scanner(path);
			lineScanner.useDelimiter(",");
			String[] store = new String[4];
			int count = 0;
			while (lineScanner.hasNext()) {
				String s = lineScanner.next();
				store[count] = s;
				count++;
			}
			lineScanner.close();

			users.add(store);
			serverLog.setText(serverLog.getText()+"S_BackEnd -> SUCCESS >>> adding ");
			for (int i = 0; i < store.length; i++) {
				if (i == 2) {
					serverLog.setText(serverLog.getText()+store[i] +" to users ArrayList.\n");
				}
				else {
					serverLog.setText(serverLog.getText()+store[i] + ", ");
				}
			}
		}
		userScanner.close();
		serverLog.setText(serverLog.getText()+"S_BackEnd -> COMPLETE >>> Finished parsing user data.\n");
		
		serverLog.setText(serverLog.getText()+"S_BackEnd -> Attempting to read in friend data...\n");
		FileInputStream fFriends = null;
		try {
			fFriends = new FileInputStream("friends.txt");
			serverLog.setText(serverLog.getText()+"S_BackEnd -> SUCCESS >>> friend data successfully read in.");
		} catch (FileNotFoundException e) {
			serverLog.setText(serverLog.getText()+"S_BackEnd -> ERROR >>> unable to locate friends.txt.");
			e.printStackTrace();
		}
		
		serverLog.setText(serverLog.getText()+"S_BackEnd -> Attempting to parse friend data to internal variable...\n");
		Scanner friendScanner = new Scanner(fFriends);
		userFriends = new Hashtable<String, ArrayList<String[]>>();

		while (friendScanner.hasNext()) {
			String path = friendScanner.next();

			Scanner lineScanner = new Scanner(path);
			lineScanner.useDelimiter(",");
			ArrayList<Integer> store = new ArrayList<Integer>();
			while (lineScanner.hasNextInt()) {
				store.add(lineScanner.nextInt());
			}
			lineScanner.close();
			
			int i = 0;
			
			serverLog.setText(serverLog.getText()+"S_BackEnd -> SUCCESS >>> adding ");
			ArrayList<String[]> friendStore = new ArrayList<String[]>();
			String user = null;
			while (i < store.size()) {

				if (store.get(i) == -2){
					user = users.get(i)[0];
					if (i != store.size()-1) {
						i++;
					}
					else {
						userFriends.put(user, friendStore);
						i++;
					}
				}
				else if (i == store.get(i)) {
					String[] temp = new String[2];
					temp[0] = users.get(i)[0];
					temp[1] = users.get(i)[1];
					friendStore.add(temp);
					if (i == store.size()-1) {
						serverLog.setText(serverLog.getText()+temp[0] +" to "+ user +"'s friends 2D array.\n");
					}
					else {
						serverLog.setText(serverLog.getText()+temp[0] + ", ");
					}

					i = store.get(i)+1;
					
					if (i == store.size() - 1) {
						userFriends.put(user, friendStore);
					}
				}
				else if (i == store.size() - 1) {
					userFriends.put(user, friendStore);
					i++;
				}
				else {
					i++;
				}				
			}
		}
		friendScanner.close();
		serverLog.setText(serverLog.getText()+"S_BackEnd -> COMPLETE >>> Finished parsing user data.\n");
		
		serverLog.setText(serverLog.getText()+"S_BackEnd -> COMPLETE >>> Finished intialisation.\n");
	}
	
	protected void listen (){
		/* Connection stuff */
		int port = 5000;
		serverLog.setText(serverLog.getText()+"S_BackEnd -> Attempting to create a new ServerSocket with port "+ port +"...\n");
		ServerSocket ss = null;
		try {
			ss = new ServerSocket(port); // FIXME extra catches here?
			serverLog.setText(serverLog.getText()+"S_BackEnd -> SUCCESS >>> new ServerSocket created with port "+ port +".\n");
		} 
		catch (BindException e) {
			serverLog.setText(serverLog.getText()+"S_BackEnd -> ERROR >>> unable to bind ServerSocket with port "+ port +".\n");
			e.printStackTrace();
		}
		catch (IOException e) {
			serverLog.setText(serverLog.getText()+"S_BackEnd -> ERROR >>> unable to create ServerSocket with port "+ port +".\n");
			e.printStackTrace();
		}


		// Tell the world we're ready to go
		serverLog.setText(serverLog.getText()+"S_BackEnd -> Started listening on "+ ss +".\n");
		// Keep accepting connections forever
		while (true) {
			serverLog.setText(serverLog.getText()+"S_BackEnd -> Wating for new connection...\n");
			// Grab the next incoming connection
			Socket s = null;
			try {
				s = ss.accept();
				serverLog.setText(serverLog.getText()+"S_BackEnd -> SUCCESS >>> new connection detected and Socket created.\n");
			} catch (IOException e) {
				serverLog.setText(serverLog.getText()+"S_BackEnd -> ERROR >>> unable to create Socket.\n");
				e.printStackTrace();
			}
			// Tell the world we've got it
			serverLog.setText(serverLog.getText()+"S_BackEnd -> Connection from "+ s +"!\n");
			// Create a DataOutputStream for writing data to the
			// other side
			serverLog.setText(serverLog.getText()+"S_BackEnd -> Attempting to get the DataInputStream & DataOutputStream associated with the new Socket...\n");
			DataOutputStream dout = null;
			DataInputStream din = null;
			try {
				dout = new DataOutputStream(s.getOutputStream());
				din = new DataInputStream(s.getInputStream());
				serverLog.setText(serverLog.getText()+"S_BackEnd -> SUCCESS >>> DataInputStream & DataOuputStream acquired.\n");
			} catch (IOException e) {
				serverLog.setText(serverLog.getText()+"S_BackEnd -> ERROR >>> unable to acquire the associated DataInputStream & DataOuputStream.\n");
				e.printStackTrace();
			}
			// Save this stream so we don't need to make it again
			serverLog.setText(serverLog.getText()+"S_BackEnd -> Saving DataOutputStream in outputStreams and outputStreamsToUsers hashtables.\n");
			outputStreams.put(s, dout);
			
			String request = null;
			while (true) {
				try {
					request = din.readUTF();
				}
				catch (IOException e) {
					serverLog.setText(serverLog.getText()+"S_BackEnd -> ERROR >>> cannout find DataOutputStream.\n");
					removeConnection(s);
					break;
					//e.printStackTrace();
				}

				Scanner requestScanner = new Scanner(request);
				requestScanner.useDelimiter(", ");

				if (requestScanner.next().compareTo("getUserData") == 0) {
					sendUserData(requestScanner.next(), dout);
					requestScanner.close();
				}
				else {
					requestScanner.close();
					break;
				}
			}
			
			if (!s.isClosed()) {
				try {
					dout.writeUTF("identify");
				} 
				catch (IOException e) {
					serverLog.setText(serverLog.getText()+"S_BackEnd -> ERROR >>> cannout find DataOutputStream.\n");
					e.printStackTrace();
				}

				String response = null;
				try {
					response = din.readUTF();
				}
				catch (IOException e) {
					serverLog.setText(serverLog.getText()+"S_BackEnd -> ERROR >>> cannout find DataOutputStream.\n");
					e.printStackTrace();
				}

				Scanner responseScanner = new Scanner(response);
				responseScanner.next();
				outputStreamsToUsers.put(responseScanner.next(), dout);
				responseScanner.close();

				// Create a new thread for this connection, and then forget
				// about it
				serverLog.setText(serverLog.getText()+"S_BackEnd -> Creating new S_SocketThread to deal with the additional work.\n");
				new S_SocketThread(this, s);
			}
		}
	}

	/* Accessors */

	// Get an enumeration of all the OutputStreams, one for each client
	// connected to us
	protected Hashtable<Socket, DataOutputStream> getOutputStreams() {
		return outputStreams;
	}
	
	protected ArrayList<String[]> getUsers() {
		return users;
	}
	
	protected String[] getUsernames() {
		String[] usernames = new String[users.size()];
		
		int count = 0;
		for (int i = 0; i <= users.size()-1; i++) {
			usernames[count] = users.get(i)[0];
			count++;
		}
		
		return usernames;
	}
	
	protected String[] getUserComputers() {
		String[] computerNames = new String[users.size()];
		
		int count = 0;
		for (int i = 0; i <= users.size()-1; i++) {
			computerNames[count] = users.get(i)[1];
			count++;
		}
		
		return computerNames;
	}
	
	protected String[] getUserEmails() {
		String[] emails = new String[users.size()];
		
		int count = 0;
		for (int i = 0; i <= users.size()-1; i++) {
			emails[count] = users.get(i)[2];
			count++;
		}
		
		return emails;
	}

	protected Hashtable<String, ArrayList<String[]>> getUserFriends() {
		return userFriends;
	}
	
	protected ArrayList<String[]> getUsersFriends(int u) {
		String name = getUsers().get(u)[0];
		return getUserFriends().get(name);
	}
	
	protected ArrayList<String[]> getUsersFriends(String s) {
		ArrayList<String[]> friends = userFriends.get(s);

		return friends;
	}
	
	protected ServerSocket getServerSocket() {
		return ss;
	}
	

	protected Socket getSocket() {
		return s;
	}
	
	/* Mutators */

	protected void setSocket(Socket s) {
		this.s = s;
	}

	protected void setServerSocket(ServerSocket ss) {
		this.ss = ss;
	}

	protected void setUsers(String[] u, int i) {
		users.set(i, u);
	}
	
	protected void setUsers(ArrayList<String[]> users) {
		this.users = users;
	}
	
	protected void setUserFriends(ArrayList<String[]> friends, int u) {
		userFriends.put(users.get(u)[0], friends);
	}
	
	protected void setServerLog(JEditorPane s) {
		serverLog = s;	
	}

	protected void setUserFriends(int[][] userFriends) {
		//this.userFriends = userFriends;
		System.out.println("setUserFriends()");
	}
	
	/* Client-server integration methods */

	protected void addFriend(String sender, String recipient) {
		
		String[] r = new String[2];
		for (int i = 0; i < users.size(); i++) {
			if (users.get(i)[0].compareTo(recipient) == 0) {
				r[0] = users.get(i)[0];
				r[1] = users.get(i)[1];
			}
		}
		
		userFriends.get(sender).add(r);
		
		sendFriendRequest(recipient, sender);
		sendUsersFriends(sender);		
	}
	
	/**
	 * 
	 * (Deprecated as of global user/friends list?)
	 * @param sender
	 * @param recipient
	 */
	protected void acceptFriend(String sender, String recipient) {
		
		String[] s = new String[2];
		for (int i = 0; i < users.size(); i++) {
			if (users.get(i)[0].compareTo(sender) == 0) {
				s[0] = users.get(i)[0];
				s[1] = users.get(i)[1];
			}
		}
		
		userFriends.get(recipient).add(s);
		
		sendUsersFriends(sender);
	}
	
	/**
	 * 
	 * (Deprecated as of global user/friends list?)
	 * @param sender
	 * @param recipient
	 */
	protected void declineFriend(String sender, String recipient) {
		
		String[] s = new String[2];
		for (int i = 0; i < users.size(); i++) {
			if (users.get(i)[0].compareTo(sender) == 0) {
				s[0] = users.get(i)[0];
				s[1] = users.get(i)[1];
			}
		}
		
		userFriends.get(recipient).remove(s);
		
		sendUsersFriends(sender);
	}
	
	protected void newUser(String n, String c, String e) {
		String[] user = {n, c, e};
		users.add(user);
		userFriends.put(n, new ArrayList<String[]>());
	}

	/* Networking methods */
	
	// Remove a socket, and it's corresponding output stream, from our
	// list. This is usually called by a connection thread that has
	// discovered that the connectin to the client is dead.
	protected void removeConnection(Socket s) {
		// Synchronize so we don't mess up sendToAll() while it walks
		// down the list of all output streamsa
		synchronized( outputStreams ) {

			// Remove it from our hashtable/list
			serverLog.setText(serverLog.getText()+"S_BackEnd -> Attempting to remove "+ s +"'s associated outputStream from internal variables...\n");
			outputStreams.remove(s);
			serverLog.setText(serverLog.getText()+"S_BackEnd -> SUCCESS >>> "+ s +"'s associated outputStream removed from internal variables.\n");
			// Make sure it's closed
			serverLog.setText(serverLog.getText()+"S_BackEnd -> Attempting to remove connection to "+ s +"...\n");
			try {
				s.close();
				serverLog.setText(serverLog.getText()+"S_BackEnd -> SUCCESS >>> socket "+ s +" has been successfully closed.\n");
			} 
			catch( IOException ie ) {
				serverLog.setText(serverLog.getText()+"ERROR >>> unable to close socket "+ s +".\n");
				ie.printStackTrace();
			}
		}
	}
	
	// Send a message to all clients (utility routine)
	protected void sendToUser(String s, String r, String m) {
		// We synchronize on this because another thread might be
		// calling removeConnection() and this would screw us up
		// as we tried to walk through the list
		synchronized(outputStreams) {
			serverLog.setText(serverLog.getText()+"S_BackEnd -> Attempting to retrive "+ s +"'s DataOutputStream...\n");

			System.out.println("S_BackEnd (sendToUser) -> s = "+ s +", r = " + r + ", m = "+ m +".");
			
			DataOutputStream dout = outputStreamsToUsers.get(r);
			
			serverLog.setText(serverLog.getText()+"S_BackEnd -> SUCCESS >>> DataOutputStream acquired.\n");

			serverLog.setText(serverLog.getText()+"S_BackEnd -> Preparing message to write to DataOutputStream.\n");
			String message = "message, "+ s +", "+ m;
			System.out.println(message);

			// ... and send the message
			serverLog.setText(serverLog.getText()+"S_BackEnd -> Attempting to send message on DataOutputStream...\n");
			try {
				dout.writeUTF(message);
				serverLog.setText(serverLog.getText()+"S_BackEnd -> SUCCESS >>> message sent on DataOutputStream successfully.\n");
			} 
			catch( IOException ie ) { 
				serverLog.setText(serverLog.getText()+"S_BackEnd -> ERROR >>> unable to send message on DataOutputStream.\n");
				System.out.println(ie); 
			}
		}
	}
	
	protected void sendUsersFriends(String s) {
		
		synchronized(outputStreams) {
			serverLog.setText(serverLog.getText()+"S_BackEnd -> Attempting to retrive "+ s +"'s DataOutputStream...\n");

			DataOutputStream dout = outputStreamsToUsers.get(s);
			
			serverLog.setText(serverLog.getText()+"S_BackEnd -> SUCCESS >>> DataOutputStream acquired.\n");

			serverLog.setText(serverLog.getText()+"S_BackEnd -> Getting sender's friend list.\n");
			ArrayList<String[]> friends = userFriends.get(s);
			System.out.println(friends.size());

			// ... and send the message
			serverLog.setText(serverLog.getText()+"S_BackEnd -> Attempting to send friend list data on DataOutputStream...\n");
			try {
				for (int i = 0; i <= friends.size() - 1; i++) {
					dout.writeUTF("friendUpdate, "+ friends.get(i)[0] +", "+ friends.get(i)[1]);
				}
				serverLog.setText(serverLog.getText()+"S_BackEnd -> SUCCESS >>> friend list data sent on DataOutputStream successfully.\n");
			} 
			catch( IOException ie ) { 
				serverLog.setText(serverLog.getText()+"S_BackEnd -> ERROR >>> unable to send friend list on DataOutputStream.\n");
				System.out.println( ie ); 
			}
		}
	}

	protected void sendFriendRequest(String r, String s) {
		
		synchronized(outputStreams) {
			serverLog.setText(serverLog.getText()+"S_BackEnd -> Attempting to retrive "+ r +"'s DataOutputStream...\n");

			DataOutputStream dout = outputStreamsToUsers.get(r);
			
			serverLog.setText(serverLog.getText()+"S_BackEnd -> SUCCESS >>> DataOutputStream acquired.\n");

			serverLog.setText(serverLog.getText()+"S_BackEnd -> Prepareing friend request message for transmission.\n");
			String message = "friendRequest, "+ s;

			// ... and send the message
			serverLog.setText(serverLog.getText()+"S_BackEnd -> Attempting to send friend request on DataOutputStream...\n");
			try {
				dout.writeUTF(message);
				serverLog.setText(serverLog.getText()+"S_BackEnd -> SUCCESS >>> friend request sent on DataOutputStream successfully.\n");
			} 
			catch( IOException ie ) { 
				serverLog.setText(serverLog.getText()+"S_BackEnd -> ERROR >>> unable to send friend request on DataOutputStream.\n");
				System.out.println( ie ); 
			}
		}
		
	}

	private void sendUserData(String sender, DataOutputStream dout) {

		synchronized(outputStreams) {
			
			String[] userData = new String[2];
			for (int i = 0; i < users.size(); i++) {
				System.out.println(sender +" - "+ users.get(i)[2]);
				if (users.get(i)[2].compareTo(sender) == 0) {
					userData[0] = users.get(i)[0];
					userData[1] = users.get(i)[3];
				}
			}
			
			serverLog.setText(serverLog.getText()+"S_BackEnd -> SUCCESS >>> DataOutputStream acquired.\n");

			// ... and send the message
			serverLog.setText(serverLog.getText()+"S_BackEnd -> Attempting to send user data on DataOutputStream...\n");
			try {
				dout.writeUTF("userData, "+ userData[0] +", "+ userData[1]);
				serverLog.setText(serverLog.getText()+"S_BackEnd -> SUCCESS >>> user data sent on DataOutputStream successfully.\n");
			} 
			catch( IOException ie ) { 
				serverLog.setText(serverLog.getText()+"S_BackEnd -> ERROR >>> unable to user data on DataOutputStream.\n");
				System.out.println( ie ); 
			}
		}
		
	}
}